What is reflect-metadata?
The reflect-metadata npm package is a library that allows developers to perform metadata reflection operations in JavaScript. It is based on the metadata reflection API that is proposed for ECMAScript and is used by frameworks like Angular and decorators to add and read metadata annotations.
What are reflect-metadata's main functionalities?
Metadata Reflection
Allows defining metadata on a class or property. The code sample shows how to define custom metadata on a class 'MyClass' with a key 'custom' and a value 'metadataValue'.
Reflect.defineMetadata('custom', 'metadataValue', MyClass);
Metadata Retrieval
Enables retrieval of metadata from a class or property. The code sample demonstrates how to retrieve metadata from a class 'MyClass' using the key 'custom'.
let metadataValue = Reflect.getMetadata('custom', MyClass);
Metadata Deletion
Provides functionality to delete metadata from a class or property. The code sample illustrates how to delete metadata associated with a class 'MyClass' using the key 'custom'.
Reflect.deleteMetadata('custom', MyClass);
Other packages similar to reflect-metadata
core-decorators
core-decorators is a library of decorators for JavaScript/TypeScript that provides similar functionality to reflect-metadata. It offers decorators for various purposes such as memoization, autobinding, and deprecation, but does not provide the same low-level metadata reflection API.
class-transformer
class-transformer is a package that allows developers to transform plain objects to class instances and vice versa. It uses decorators to annotate classes and properties, similar to how reflect-metadata uses metadata, but it focuses more on the transformation aspect rather than the reflection of metadata.
class-validator
class-validator is a package that uses decorators to perform validation on class properties. It leverages metadata to store validation rules, which is a concept similar to reflect-metadata. However, its primary focus is on validation rather than general metadata reflection.
Metadata Reflection API
Installation
npm install reflect-metadata
Background
- Decorators add the ability to augment a class and its members as the class is defined, through a declarative syntax.
- Traceur attaches annotations to a static property on the class.
- Languages like C# (.NET), and Java support attributes or annotations that add metadata to types, along with a reflective API for reading metadata.
Goals
- A number of use cases (Composition/Dependency Injection, Runtime Type Assertions, Reflection/Mirroring, Testing) want the ability to add additional metadata to a class in a consistent manner.
- A consistent approach is needed for various tools and libraries to be able to reason over metadata.
- Metadata-producing decorators (nee. "Annotations") need to be generally composable with mutating decorators.
- Metadata should be available not only on an object but also through a Proxy, with related traps.
- Defining new metadata-producing decorators should not be arduous or over-complex for a developer.
- Metadata should be consistent with other language and runtime features of ECMAScript.
Syntax
- Declarative definition of metadata:
class C {
@Reflect.metadata(metadataKey, metadataValue)
method() {
}
}
- Imperative definition of metadata:
Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");
- Imperative introspection of metadata:
let obj = new C();
let metadataValue = Reflect.getMetadata(metadataKey, obj, "method");
Semantics
- Object has a new [[Metadata]] internal property that will contain a Map whose keys are property keys (or undefined) and whose values are Maps of metadata keys to metadata values.
- Object will have a number of new internal methods for [[DefineOwnMetadata]], [[GetOwnMetadata]], [[HasOwnMetadata]], etc.
- These internal methods can be overridden by a Proxy to support additional traps.
- These internal methods will by default call a set of abstract operations to define and read metadata.
- The Reflect object will expose the MOP operations to allow imperative access to metadata.
- Metadata defined on class declaration C is stored in C.[[Metadata]], with undefined as the key.
- Metadata defined on static members of class declaration C are stored in C.[[Metadata]], with the property key as the key.
- Metadata defined on instance members of class declaration C are stored in C.prototype.[[Metadata]], with the property key as the key.
API
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);
@Reflect.metadata(metadataKey, metadataValue)
class C {
@Reflect.metadata(metadataKey, metadataValue)
method() {
}
}
Alternatives
- Use properties rather than a separate API.
- Obvious downside is that this can be a lot of code:
function ParamTypes(...types) {
return (target, propertyKey) => {
const symParamTypes = Symbol.for("design:paramtypes");
if (propertyKey === undefined) {
target[symParamTypes] = types;
}
else {
const symProperties = Symbol.for("design:properties");
let properties, property;
if (Object.prototype.hasOwnProperty.call(target, symProperties)) {
properties = target[symProperties];
}
else {
properties = target[symProperties] = {};
}
if (Object.prototype.hasOwnProperty.call(properties, propertyKey)) {
property = properties[propertyKey];
}
else {
property = properties[propertyKey] = {};
}
property[symParamTypes] = types;
}
};
}
Notes
- Though it may seem counterintuitive, the methods on Reflect place the parameters for the metadata key and metadata value before the target or property key. This is due to the fact that the property key is the only optional parameter in the argument list. This also makes the methods easier to curry with Function#bind. This also helps reduce the overall footprint and complexity of a metadata-producing decorator that could target both a class or a property:
function ParamTypes(...types) {
return (target, propertyKey) => { Reflect.defineMetadata("design:paramtypes", types, target, propertyKey); }
}
- To enable experimental support for metadata decorators in your TypeScript project, you must add
"experimentalDecorators": true
to your tsconfig.json file. - To enable experimental support for auto-generated type metadata in your TypeScript project, you must add
"emitDecoratorMetadata": true
to your tsconfig.json file.
- Please note that auto-generated type metadata may have issues with circular or forward references for types.
Issues
- A poorly written mutating decorator for a class constructor could cause metadata to become lost if the prototype chain is not maintained. Though, not maintaining the prototype chain in a mutating decorator for a class constructor would have other negative side effects as well. @rbuckton
- This is mitigated if the mutating decorator returns a class expression that extends from the target, or returns a proxy for the decorator. @rbuckton
- Metadata for a method is attached to the class (or prototype) via the property key. It would not then be available if trying to read metadata on the function of the method (e.g. "tearing-off" the method from the class). @rbuckton